using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using AElf.Contracts.Consensus.AEDPoS;
using AElf.Contracts.Election;
using AElf.Contracts.MultiToken;
using AElf.Contracts.Profit;
using AElf.ContractTestKit.AEDPoSExtension;
using AElf.Cryptography.ECDSA;
using AElf.CSharp.Core.Extension;
using AElf.EconomicSystem;
using AElf.GovernmentSystem;
using AElf.Kernel.Consensus;
using AElf.Types;
using Google.Protobuf.WellKnownTypes;
using Shouldly;

namespace AElf.Contracts.Economic.AEDPoSExtension.Tests;

/// <summary>
///     Some view methods.
/// </summary>
public partial class EconomicTestBase
{
    public enum SchemeType
    {
        Treasury,

        MinerReward,
        BackupSubsidy,
        CitizenWelfare,

        MinerBasicReward,
        WelcomeReward,
        FlexibleReward
    }

    protected async Task<long> GetBalanceAsync(Address owner)
    {
        return (await TokenStub.GetBalance.CallAsync(new GetBalanceInput
        {
            Owner = owner,
            Symbol = EconomicTestConstants.TokenSymbol
        })).Balance;
    }

    internal async Task<DistributedProfitsInfo> GetDistributedInformationAsync(Hash schemeId, long period)
    {
        return await ProfitStub.GetDistributedProfitsInfo.CallAsync(new SchemePeriod
        {
            SchemeId = schemeId,
            Period = period
        });
    }

    internal async Task<Dictionary<SchemeType, Scheme>> GetTreasurySchemesAsync()
    {
        var treasurySchemeId = await TreasuryStub.GetTreasurySchemeId.CallAsync(new Empty());
        var schemes = new Dictionary<SchemeType, Scheme>();
        var treasuryScheme = await ProfitStub.GetScheme.CallAsync(treasurySchemeId);
        schemes.Add(SchemeType.Treasury, treasuryScheme);
        var minerRewardScheme = await ProfitStub.GetScheme.CallAsync(treasuryScheme.SubSchemes[0].SchemeId);
        schemes.Add(SchemeType.MinerReward, minerRewardScheme);
        schemes.Add(SchemeType.BackupSubsidy,
            await ProfitStub.GetScheme.CallAsync(treasuryScheme.SubSchemes[1].SchemeId));
        schemes.Add(SchemeType.CitizenWelfare,
            await ProfitStub.GetScheme.CallAsync(treasuryScheme.SubSchemes[2].SchemeId));
        schemes.Add(SchemeType.MinerBasicReward,
            await ProfitStub.GetScheme.CallAsync(minerRewardScheme.SubSchemes[0].SchemeId));
        schemes.Add(SchemeType.WelcomeReward,
            await ProfitStub.GetScheme.CallAsync(minerRewardScheme.SubSchemes[1].SchemeId));
        schemes.Add(SchemeType.FlexibleReward,
            await ProfitStub.GetScheme.CallAsync(minerRewardScheme.SubSchemes[2].SchemeId));
        return schemes;
    }

    internal async Task ClaimProfits(IEnumerable<ECKeyPair> keyPairs, Hash schemeId)
    {
        var stubs = ConvertKeyPairsToProfitStubs(keyPairs);
        await BlockMiningService.MineBlockAsync(stubs.Select(s =>
            s.ClaimProfits.GetTransaction(new ClaimProfitsInput
            {
                SchemeId = schemeId
            })).ToList());
    }

    /// <summary>
    ///     Tolerance: 10
    /// </summary>
    /// <param name="keyPairs"></param>
    /// <param name="shouldIncrease"></param>
    /// <param name="balancesBefore"></param>
    /// <returns></returns>
    internal async Task CheckBalancesAsync(IEnumerable<ECKeyPair> keyPairs, long shouldIncrease,
        Dictionary<ECKeyPair, long> balancesBefore = null)
    {
        const long tolerance = 20;
        balancesBefore ??= new Dictionary<ECKeyPair, long>();
        foreach (var keyPair in keyPairs)
        {
            var amount = await GetBalanceAsync(Address.FromPublicKey(keyPair.PublicKey));
            amount.ShouldBeGreaterThanOrEqualTo(shouldIncrease + balancesBefore[keyPair] - tolerance);
            amount.ShouldBeLessThanOrEqualTo(shouldIncrease + balancesBefore[keyPair] + tolerance);
        }
    }

    internal List<AEDPoSContractImplContainer.AEDPoSContractImplStub> ConvertKeyPairsToConsensusStubs(
        IEnumerable<ECKeyPair> keyPairs)
    {
        return keyPairs.Select(p =>
            GetTester<AEDPoSContractImplContainer.AEDPoSContractImplStub>(
                ContractAddresses[ConsensusSmartContractAddressNameProvider.Name], p)).ToList();
    }

    internal List<ProfitContractContainer.ProfitContractStub> ConvertKeyPairsToProfitStubs(
        IEnumerable<ECKeyPair> keyPairs)
    {
        return keyPairs.Select(p =>
            GetTester<ProfitContractContainer.ProfitContractStub>(
                ContractAddresses[ProfitSmartContractAddressNameProvider.Name], p)).ToList();
    }

    internal List<ElectionContractContainer.ElectionContractStub> ConvertKeyPairsToElectionStubs(
        IEnumerable<ECKeyPair> keyPairs)
    {
        return keyPairs.Select(p =>
            GetTester<ElectionContractContainer.ElectionContractStub>(
                ContractAddresses[ElectionSmartContractAddressNameProvider.Name], p)).ToList();
    }

    internal List<Transaction> GetVoteTransactions(int timesOfMinimumLockDays,
        long amount, string candidatePubkey, int votersCount = 0)
    {
        if (votersCount > AEDPoSExtensionConstants.CitizenKeyPairsCount)
            throw new ArgumentOutOfRangeException(nameof(votersCount),
                $"Didn't prepare this amount of citizens. {votersCount} > {AEDPoSExtensionConstants.CitizenKeyPairsCount}");

        votersCount = votersCount == 0 ? AEDPoSExtensionConstants.CitizenKeyPairsCount : votersCount;

        var voteTransaction = new List<Transaction>();

        var blockTime = TestDataProvider.GetBlockTime();

        var electionStubs = ConvertKeyPairsToElectionStubs(MissionedECKeyPairs.CitizenKeyPairs.Take(votersCount));

        foreach (var electionStub in electionStubs)
            voteTransaction.Add(electionStub.Vote.GetTransaction(new VoteMinerInput
            {
                CandidatePubkey = candidatePubkey,
                Amount = amount,
                EndTimestamp =
                    blockTime.AddSeconds(timesOfMinimumLockDays * EconomicTestConstants.MinimumLockTime)
            }));

        return voteTransaction;
    }

    internal async Task<long> MineBlocksToNextTermAsync(long currentTermNumber = 0)
    {
        currentTermNumber = currentTermNumber == 0
            ? (await ConsensusStub.GetCurrentTermNumber.CallAsync(new Empty())).Value
            : currentTermNumber;
        var targetTermNumber = currentTermNumber + 1;
        var actualTermNumber = 0L;
        while (actualTermNumber != targetTermNumber)
        {
            BlockMiningService.SkipTime(1);
            await BlockMiningService.MineBlockAsync();
            actualTermNumber = (await ConsensusStub.GetCurrentTermNumber.CallAsync(new Empty())).Value;
        }

        return (await ConsensusStub.GetMinedBlocksOfPreviousTerm.CallAsync(new Empty())).Value;
    }
}